<!DOCTYPE html>
<!--
Copyright 2010 WebDriver committers
Copyright 2010 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
  <title>Unit Tests for bot.inject</title>
  <script src="test_bootstrap.js"></script>
  <script type="text/javascript">
    goog.require('bot.Error');
    goog.require('bot.ErrorCode');
    goog.require('bot.inject');
    goog.require('bot.inject.cache');
    goog.require('bot.json');
    goog.require('bot.userAgent');
    goog.require('goog.Promise');
    goog.require('goog.dom');
    goog.require('goog.events');
    goog.require('goog.testing.jsunit');
    goog.require('goog.userAgent');
    goog.require('goog.userAgent.product');
  </script>
</head>
<body>
  <div style="display:none">
    <iframe id="test-frame" src="testdata/blank_page.html"></iframe>
  </div>
  <script type="text/javascript">
    goog.global.MY_GLOBAL_CONSTANT = 123;

    var frame, frameWin, frameDoc;

    function setUpPage() {
      goog.testing.TestCase.getActiveTestCase().promiseTimeout = 30000; // 30s
    }

    function setUp() {
      frame = goog.dom.$('test-frame');
      frameWin = goog.dom.getFrameContentWindow(frame);
      frameDoc = goog.dom.getFrameContentDocument(frame);

      // Force the cache to be recreated on the next access. We cannot "delete"
      // it here because IE complains about it being an unsupported operation.
      document[bot.inject.cache.CACHE_KEY_] = null;

      // Delete the cache in our test frame, if it was created.
      frameDoc[bot.inject.cache.CACHE_KEY_] = null;
    }

    function testAddWindowToCache() {
      var id = bot.inject.cache.addElement(frameWin);
      assertEquals(frameWin, bot.inject.cache.getElement(id));
    }


    function testAddWindowToCacheMultipleTimesReusesIds() {
      var id1 = bot.inject.cache.addElement(frameWin);
      var id2 = bot.inject.cache.addElement(frameWin);
      assertEquals(id1, id2);
    }


    function testGetElementThrowsIfWindowIsClosed() {
      var win = {document: 1, closed: 1};
      var id = bot.inject.cache.addElement(win);
      assertThrows(goog.partial(bot.inject.cache.getElement, id));
    }


    function testShouldWrapAndUnwrapWindow() {
      var wrapped = bot.inject.wrapValue(frameWin);
      assertNotNull(wrapped);
      assertNotUndefined(wrapped);
      assertTrue(goog.isObject(wrapped));
      assertTrue(goog.object.containsKey(wrapped, bot.inject.WINDOW_KEY));

      var id = wrapped[bot.inject.WINDOW_KEY];
      var res = bot.inject.cache.getElement(id);
      assertEquals(frameWin, res);
      assertEquals(frameWin, bot.inject.unwrapValue(wrapped));
    }


    function testShouldBeAbleToCacheElements() {
      var id = bot.inject.cache.addElement(document.body);
      var el = bot.inject.cache.getElement(id);
      assertEquals(document.body, el);
    }


    function testShouldReuseExistingIdIfElementAlreadyExistsInTheCache() {
      var id1 = bot.inject.cache.addElement(document.body);
      var id2 = bot.inject.cache.addElement(document.body);
      assertEquals(id1, id2);
    }


    function testShouldThrowIfElementDoesNotExistInTheCache() {
      assertThrows(goog.partial(bot.inject.cache.getElement, 'not-there'));
    }


    function testShouldDecodeIdsWhenRetrievingFromTheCache() {
      var id = bot.inject.cache.addElement(document.body);
      id = encodeURIComponent(id);
      var el = bot.inject.cache.getElement(id);
      assertEquals(document.body, el);
    }


    function testShouldThrowIfCachedElementIsNoLongerAttachedToTheDom() {
      if (goog.userAgent.IE) {
        // TODO: Don't skip this.
        return;
      }
      var div = document.createElement('DIV');
      document.body.appendChild(div);

      var id = bot.inject.cache.addElement(div);
      assertEquals(div, bot.inject.cache.getElement(id));

      document.body.removeChild(div);
      assertThrows(goog.partial(bot.inject.cache.getElement, id));
    }


    function testDoesNotWrapStringBooleansNumbersOrNull() {
      assertEquals('foo', bot.inject.wrapValue('foo'));
      assertEquals(123, bot.inject.wrapValue(123));
      assertTrue(bot.inject.wrapValue(true));
      assertNull(bot.inject.wrapValue(null));
    }


    function testConvertsUndefinedValueToNullForWrapping() {
      assertNull(bot.inject.wrapValue(undefined));
    }


    function testShouldAddElementsToCacheWhenWrapping() {
      var wrapped = bot.inject.wrapValue(document.body);
      assertNotNull(wrapped);
      assertNotUndefined(wrapped);
      assertTrue(goog.isObject(wrapped));
      assertTrue(goog.object.containsKey(wrapped, bot.inject.ELEMENT_KEY));

      var id = wrapped[bot.inject.ELEMENT_KEY];
      var el = bot.inject.cache.getElement(id);
      assertEquals(document.body, el);
    }


    function testShouldRecursivelyWrapArrays() {
      var unwrapped = [123, 'abc', null, document.body];
      var wrapped = bot.inject.wrapValue(unwrapped);

      assertEquals(unwrapped.length, wrapped.length);
      assertEquals(unwrapped[0], wrapped[0]);
      assertEquals(unwrapped[1], wrapped[1]);
      assertEquals(unwrapped[2], wrapped[2]);

      assertTrue(goog.object.containsKey(wrapped[3], bot.inject.ELEMENT_KEY));
      assertEquals(unwrapped[3], bot.inject.cache.getElement(
          wrapped[3][bot.inject.ELEMENT_KEY]));
    }


    function testShouldBeAbleToWrapNodeListsAsArrays() {
      var unwrapped = document.getElementsByTagName('body');
      var wrapped = bot.inject.wrapValue(unwrapped);

      assertTrue('should return an array, but was ' + goog.typeOf(wrapped),
          goog.isArray(wrapped));
      assertEquals(unwrapped.length, wrapped.length);
      assertTrue(goog.object.containsKey(wrapped[0], bot.inject.ELEMENT_KEY));
      assertEquals(document.body, bot.inject.cache.getElement(
          wrapped[0][bot.inject.ELEMENT_KEY]));
    }


    function testShouldThrowWhenWrappingRecursiveStructure() {
      var obj1 = {};
      var obj2 = {};
      obj1['obj2'] = obj2;
      obj2['obj1'] = obj1;
      assertThrows(goog.partial(bot.inject.wrapValue, obj1));
    }


    function testShouldBeAbleToUnWrapWrappedValues() {
      var unwrapped = [123, 'abc', null, document.body];
      var wrapped = bot.inject.wrapValue(unwrapped);
      var roundTripped = bot.inject.unwrapValue(wrapped);
      assertArrayEquals(unwrapped, roundTripped);
    }


    function testShouldBeAbleToUnWrapNestedArrays() {
      var unwrapped = [123, 'abc', null, [document.body, null,
          [document.body]]];
      var wrapped = bot.inject.wrapValue(unwrapped);
      var roundTripped = bot.inject.unwrapValue(wrapped);
      assertArrayEquals(unwrapped, roundTripped);
    }


    function testShouldBeAbleToUnWrapNestedObjects() {
      var unwrapped = {'foo': {'bar': document.body}};
      var wrapped = bot.inject.wrapValue(unwrapped);
      var roundTripped = bot.inject.unwrapValue(wrapped);

      assertTrue(goog.object.containsKey(roundTripped, 'foo'));
      var foo = roundTripped['foo'];

      assertTrue(goog.object.containsKey(foo, 'bar'));
      assertEquals(document.body, foo['bar']);
    }


    function testShouldUnWrapElementsUsingTheGivenDocumentsCache() {
      var wrapped = bot.inject.wrapValue(frameDoc.body);
      assertNotNull(wrapped);
      assertNotUndefined(wrapped);
      assertTrue(goog.isObject(wrapped));
      assertTrue(goog.object.containsKey(wrapped, bot.inject.ELEMENT_KEY));

      // Should not be able to unwrap using our document since the element
      // was cached on its ownerDocument.
      assertThrows(goog.partial(bot.inject.unwrapValue, wrapped));

      var unwrapped = bot.inject.unwrapValue(wrapped, frameDoc);
      assertEquals(frameDoc.body, unwrapped);
    }


    function testShouldBeAbleToUnwrapArgumentsExecuteScriptAndWrapResult() {
      var id = bot.inject.cache.addElement(document.body);
      var args = [{}];
      args[0][bot.inject.ELEMENT_KEY] = id;
      var result = bot.inject.executeScript(
          function() { return arguments[0]; }, args);

      assertTrue(goog.object.containsKey(result, 'status'));
      assertEquals(bot.ErrorCode.SUCCESS, result['status']);

      assertTrue(goog.object.containsKey(result, 'value'));
      assertTrue(goog.object.containsKey(result['value'],
          bot.inject.ELEMENT_KEY));
      assertEquals(document.body, bot.inject.cache.getElement(
          result['value'][bot.inject.ELEMENT_KEY]));
    }


    function testShouldTrapAndReturnWrappedErrorsFromInjectedScripts() {
      var result = bot.inject.executeScript(
          function() { throw Error('ouch'); }, []);
      assertTrue(goog.object.containsKey(result, 'status'));
      assertTrue(goog.object.containsKey(result, 'value'));

      assertEquals(bot.ErrorCode.UNKNOWN_ERROR, result['status']);
      result = result['value'];
      assertTrue(goog.object.containsKey(result, 'message'));
      assertEquals('ouch', result['message']);
    }


    function testShouldResetCacheWhenPageIsRefreshed() {
      var id = bot.inject.cache.addElement(frameDoc.body);
      assertEquals(frameDoc.body, bot.inject.cache.getElement(id, frameDoc));

      return new goog.Promise(function(loaded) {
        goog.events.listenOnce(frame, 'load', loaded);
        frameWin.location.reload();
      }).then(function() {
        frameDoc = goog.dom.getFrameContentDocument(frame);
        assertThrows(goog.partial(bot.inject.cache.getElement, id, frameDoc));
      });
    }


    function testShouldStringifyResultsWhenAskedToDoSo() {
      var result = bot.inject.executeScript(function() {
        return arguments[0] + arguments[1];
      }, ['abc', 123], true);

      assertEquals('string', goog.typeOf(result));
      var json = bot.json.parse(result);
      assertTrue('No status: ' + result, 'status' in json);
      assertTrue('No value: ' + result, 'value' in json);
      assertEquals(0, json['status']);
      assertEquals('abc123', json['value']);
    }


    function testShouldStringifyErrorsWhenAskedForStringResults() {
      var result = bot.inject.executeScript(function() {
        throw new bot.Error(bot.ErrorCode.NO_SUCH_ELEMENT, 'ouch');
      }, [], true);

      assertEquals('string', goog.typeOf(result));
      var json = bot.json.parse(result);
      assertTrue('No status: ' + result, 'status' in json);
      assertTrue('No value: ' + result, 'value' in json);
      assertEquals(bot.ErrorCode.NO_SUCH_ELEMENT, json['status']);
      assertTrue('No message: ' + result, 'message' in json['value']);
      assertEquals('ouch', json['value']['message']);
    }


    function testShouldBeAbleToExecuteAsyncScript() {
      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript(
            'window.setTimeout(goog.partial(arguments[0], 1), 10)',
            [], 250, done);
      }).then(function(result) {
        assertTrue(goog.object.containsKey(result, 'status'));
        assertEquals(bot.ErrorCode.SUCCESS, result['status']);
        assertTrue(goog.object.containsKey(result, 'value'));
        assertEquals(1, result['value']);
      });
    }


    function testShouldBeAbleToExecuteAsyncScriptThatCallsbackSynchronously() {
      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript('arguments[0](1)', [], 0, done);
      }).then(function(result) {
        assertTrue(goog.object.containsKey(result, 'status'));
        assertEquals(bot.ErrorCode.SUCCESS, result['status']);
        assertTrue(goog.object.containsKey(result, 'value'));
        assertEquals(1, result['value']);
      });
    }


    function testShouldBeAbleToExecuteAsyncFunc() {
      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript(
            function(callback) { callback(1) }, [], 0, done);
      }).then(function(result) {
        assertTrue(goog.object.containsKey(result, 'status'));
        assertEquals(bot.ErrorCode.SUCCESS, result['status']);
        assertTrue(goog.object.containsKey(result, 'value'));
        assertEquals(1, result['value']);
      });
    }


    function testShouldThrowOnExecuteAsyncException() {
      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript('nosuchfunc()', [], 0, done);
      }).then(function(result) {
        assertTrue(goog.object.containsKey(result, 'status'));
        assertEquals(bot.ErrorCode.UNKNOWN_ERROR, result['status']);
      });
    }


    function testShouldTimeoutInExecuteAsync() {
      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript('', [], 0, done);
      }).then(function(result) {
        assertTrue(goog.object.containsKey(result, 'status'));
        assertEquals(bot.ErrorCode.SCRIPT_TIMEOUT, result['status']);
      });
    }


    function testShouldBeAbleToStringifyResult() {
      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript('', [], 0, done, true);
      }).then(function(jsonResult) {
        var result = bot.json.parse(jsonResult);
        assertTrue(goog.object.containsKey(result, 'status'));
        assertEquals(bot.ErrorCode.SCRIPT_TIMEOUT, result['status']);
      });
    }


    function testShouldBeAbleToWrapAndUnwrapInExecuteAsyncScript() {
      return new goog.Promise(function(done) {
        var id = bot.inject.cache.addElement(document.body);
        var args = [{}];
        args[0][bot.inject.ELEMENT_KEY] = id;
        bot.inject.executeAsyncScript(
            'arguments[1](arguments[0])', args, 0, done);
      }).then(function(result) {
        assertTrue(goog.object.containsKey(result, 'status'));
        assertEquals(bot.ErrorCode.SUCCESS, result['status']);

        assertTrue(goog.object.containsKey(result, 'value'));
        assertTrue(goog.object.containsKey(result['value'],
            bot.inject.ELEMENT_KEY));
        assertEquals(document.body, bot.inject.cache.getElement(
            result['value'][bot.inject.ELEMENT_KEY]));
      });
    }

    function testShouldExecuteAsyncScriptsInTheContextOfTheSpecifiedWindow() {
      if (goog.userAgent.IE || goog.userAgent.product.SAFARI ||
          (goog.userAgent.product.ANDROID &&
           !bot.userAgent.isProductVersion(4))) {
        // Android prior to Ice Cream Sandwich has a known issue, b/5006213.
        // TODO: Don't skip this.
        return;
      }

      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript(function(callback) {
          callback({
            'this == window': (this == window),
            'this == top': (this == window.top),
            'typeof window.MY_GLOBAL_CONSTANT': (typeof MY_GLOBAL_CONSTANT),
            'typeof window.top.MY_GLOBAL_CONSTANT':
                (typeof window.top.MY_GLOBAL_CONSTANT)
          });
        }, [], 0, done, false, frameWin);
      }).then(function(result) {
        var jsonResult = bot.json.stringify(result);

        assertTrue(jsonResult, goog.object.containsKey(result, 'status'));
        assertEquals(jsonResult, bot.ErrorCode.SUCCESS, result['status']);

        assertTrue(jsonResult, goog.object.containsKey(result, 'value'));
        var value = result['value'];

        assertEquals(jsonResult, true, value['this == window']);
        assertEquals(jsonResult, false, value['this == top']);
        assertEquals(jsonResult, 'undefined',
                     value['typeof window.MY_GLOBAL_CONSTANT']);
        assertEquals(jsonResult, 'number',
                     value['typeof window.top.MY_GLOBAL_CONSTANT']);
      });
    }

    function testShouldExecuteScriptsInTheContextOfTheSpecifiedWindow() {
      if (goog.userAgent.IE || goog.userAgent.product.SAFARI ||
          (goog.userAgent.product.ANDROID &&
           !bot.userAgent.isProductVersion(4))) {
        // Android prior to Ice Cream Sandwich has a known issue, b/5006213.
        // TODO: Don't skip this.
        return;
      }

      var result = bot.inject.executeScript(function() {
        return {
          'this == window': (this == window),
          'this == top': (this == window.top),
          'typeof window.MY_GLOBAL_CONSTANT': (typeof MY_GLOBAL_CONSTANT),
          'typeof window.top.MY_GLOBAL_CONSTANT':
              (typeof window.top.MY_GLOBAL_CONSTANT)
        };
      }, [], false, frameWin);

      var jsonResult = bot.json.stringify(result);

      assertTrue(jsonResult, goog.object.containsKey(result, 'status'));
      assertEquals(jsonResult, bot.ErrorCode.SUCCESS, result['status']);

      assertTrue(jsonResult, goog.object.containsKey(result, 'value'));
      var value = result['value'];

      assertEquals(jsonResult, true, value['this == window']);
      assertEquals(jsonResult, false, value['this == top']);
      assertEquals(jsonResult, 'undefined',
                   value['typeof window.MY_GLOBAL_CONSTANT']);
      assertEquals(jsonResult, 'number',
                   value['typeof window.top.MY_GLOBAL_CONSTANT']);
    }

    function testCorrectlyUnwrapsFunctionArgsForInjectedScripts() {
      var result = bot.inject.executeScript(function() {
        return arguments[0]();
      }, [function() {return 1;}]);
      assertEquals(bot.ErrorCode.SUCCESS, result['status']);
      assertEquals(1, result['value']);
    }

    function testCorrectlyUnwrapsFunctionArgsForInjectedAsyncScripts() {
      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript(function() {
          var callback = arguments[arguments.length - 1];
          callback(arguments[0]());
        }, [function() {return 1;}], 0, done, false);
      }).then(function(result) {
        assertEquals(bot.ErrorCode.SUCCESS, result['status']);
        assertEquals(1, result['value']);
      });
    }

    function testCorrectlyUnwrapsArgsForInjectedScripts() {
      if (goog.userAgent.IE) {
        // TODO: Don't skip this.
        return;
      }
      var wrapped = bot.inject.wrapValue(frameDoc.body);

      var result = bot.inject.executeScript(function() {
        return [arguments[0], arguments[0].tagName.toUpperCase()];
      }, [wrapped], false, frameWin);

      assertEquals(bot.ErrorCode.SUCCESS, result['status']);

      var value = result['value'];
      assertTrue(goog.isArray(value));
      assertEquals(2, value.length);

      assertTrue(goog.isObject(value[0]));
      assertTrue(goog.object.containsKey(value[0], bot.inject.ELEMENT_KEY));
      assertEquals(wrapped[bot.inject.ELEMENT_KEY],
                   value[0][bot.inject.ELEMENT_KEY]);

      assertEquals('BODY', value[1]);
    }

    function testCorrectlyUnwrapsArgsForInjectedAsyncScripts() {
      if (goog.userAgent.IE) {
        // TODO: Don't skip this.
        return;
      }
      var wrapped = bot.inject.wrapValue(frameDoc.body);

      return new goog.Promise(function(done) {
        bot.inject.executeAsyncScript(function(element, callback) {
          callback([element, element.tagName.toUpperCase()]);
        }, [wrapped], 0, done, false, frameWin);
      }).then(function(result) {
        assertEquals(bot.ErrorCode.SUCCESS, result['status']);

        var value = result['value'];
        assertTrue(goog.isArray(value));
        assertEquals(2, value.length);

        assertTrue(goog.isObject(value[0]));
        assertTrue(goog.object.containsKey(value[0], bot.inject.ELEMENT_KEY));
        assertEquals(wrapped[bot.inject.ELEMENT_KEY],
                     value[0][bot.inject.ELEMENT_KEY]);

        assertEquals('BODY', value[1]);
      });
    }

    function testExecuteScriptShouldBeAbleToRecurseOnItself() {
      if (goog.userAgent.IE) {
        // TODO: Don't skip this.
        return;
      }
      var json = bot.inject.executeScript(bot.inject.executeScript,
          ['return 1 +2;'], true);
      var result = bot.json.parse(json);

      assertTrue(json, goog.object.containsKey(result, 'status'));
      assertEquals(json, bot.ErrorCode.SUCCESS, result['status']);
      assertTrue(json, goog.object.containsKey(result, 'value'));

      var innerResult = result['value'];
      assertTrue(json, goog.isObject(innerResult));
      assertTrue(json, goog.object.containsKey(innerResult, 'status'));
      assertEquals(json, bot.ErrorCode.SUCCESS, innerResult['status']);
      assertTrue(json, goog.object.containsKey(innerResult, 'value'));
      assertEquals(json, 3, innerResult['value']);
    }

  </script>
</body>
</html>
